﻿//Roshan

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;

namespace Tzync
{
    /// <summary>
    /// Allows the user to restore previous versions of files that were synced to this computer
    /// </summary>
   public class Rollback
    {
        private long spaceBuffer = 100 * 1024 * 1024;   //space buffer to maintain in case computer has less space than allocated
        private SystemType currentSystem;  //computer user is on in profile (host/guest)
        private Profile currentProfile;    //profile in use
        private string rbLocation;  //location of rollback folder
        private string rbMetadataLocation;  //location of rollback metadata file
        private long spaceAllocated; //space on computer for storing rollback files     
        private long spaceRemaining;    //space left on computer for storing rollback files
        private int rbFileId; //id of rollback file for updating purposes
        private List<RollbackFile> rbFileList;  //list of rollback files
        
        /// <summary>
        /// Sets up rollback metadata for current profile
        /// </summary> 
        /// <param name="system"> System/Computer user is on</param>
        /// <param name="profile">Current profile loaded</param>
        /// <param name="location">Location of rollback files</param>
        /// <param name="space">Amt of space allocated for rollback</param>
        public Rollback(SystemType system, Profile profile, string location, long space)
        {
            //initialization
            currentSystem = system;
            currentProfile = profile;
            spaceAllocated = space;
            spaceRemaining = space;
            rbFileList = new List<RollbackFile>();
            rbLocation += location + profile.ProfileName;  //location to store rollback files for this profile
            rbMetadataLocation = rbLocation + @"\" + profile.ProfileName;   //location of rollback metadata for this profile
        }

       /// <summary>
        /// Creates and writes setup information to rollback metadata file
        /// Only on first run at computer
       /// </summary>
        public void CreateRollback()
        {
            AdjustSpaceAllocated(); //Check if there's enough space and adjust accordingly
            Directory.CreateDirectory(rbLocation);  //create rollback directory
            WriteRollbackMetadata();    //create metadata file
        }

       /// <summary>
       /// Loads rollback metadata
       /// Only on second run onwards at computer
       /// </summary>
        public void LoadRollback()
        {
            ReadRollbackMetadata(); //read from metadata
            AdjustSpaceAllocated(); //Check if there's enough space and adjust accordingly
        }

       /// <summary>
       /// Writes to rollback metadata file
       /// </summary>
        public void WriteRollbackMetadata()
        {
            StreamWriter metadataWriter = new StreamWriter(rbMetadataLocation);
            metadataWriter.WriteLine(spaceAllocated + "|" + spaceRemaining + "|" + rbFileId); //write setup information

            if (rbFileList.Count() > 0) //if there are rollback files to write to metadata
            {
                if (File.Exists(rbMetadataLocation))
                {
                    for (int i = 0; i < rbFileList.Count; i++)
                    {
                        if (rbFileList[i].RbDone == false)   //write each rollback file to rollback metadata 
                        {
                            metadataWriter.WriteLine(rbFileList[i].RbSource + "|" + rbFileList[i].RbDestination + "|" +
                                rbFileList[i].RbLastSyncTime.ToString() + "|" + rbFileList[i].RbId + "|" + rbFileList[i].RbVersion);
                        }
                        else if (rbFileList[i].RbDone == true)    //Remove files that have been rollback'ed
                            rbFileList.RemoveAt(i);
                    }
                }
                else
                    throw new FileNotFoundException(rbMetadataLocation, ActionType.Rollback);
            }

            metadataWriter.Close();
        }

        /// <summary>
        /// Reads from rollback metadata file
        /// </summary>
        public void ReadRollbackMetadata()
        {
            rbFileList.Clear();

            if (File.Exists(rbMetadataLocation))
            {
                RollbackFile rbFile;    //a rollback file
                //for processing text files
                String[] data;
                String line;
                char[] charSeparators = new char[] { '|' };     // | is the delimiter
                StreamReader metadataReader = new StreamReader(rbMetadataLocation);

                line = metadataReader.ReadLine();
                data = line.Split(charSeparators);

                spaceAllocated = Convert.ToInt32(data[0]);   //space allocated
                spaceRemaining = Convert.ToInt32(data[1]);  //space remaining
                rbFileId = Convert.ToInt32(data[2]);  //id of rollback file

                line = metadataReader.ReadLine();

                //loads rollback files into memory
                while (line != null)
                {
                    data = line.Split(charSeparators);
                    rbFile = new RollbackFile();
                    rbFile.RbSource = data[0];  //address of file in rollback folder
                    rbFile.RbDestination = data[1]; //address of file to rollback to
                    rbFile.RbLastSyncTime = data[2];    //date last synced of file
                    rbFile.RbId = rbFileId;    //id of file
                    rbFile.RbVersion = Convert.ToInt32(data[4]);    //version of file

                    rbFileList.Add(rbFile);
                    line = metadataReader.ReadLine();
                }

                metadataReader.Close();
            }
            else
                throw new FileNotFoundException(rbMetadataLocation, ActionType.Rollback);
        }

       /// <summary>
        /// Creates rollback files before sync for future rollback
        /// Only performed when there are new files which are going to overwrite files on this computer
        /// Only caters to files
       /// </summary>
       /// <param name="differenceList">List of differences to process</param>
       /// <param name="arrNodes">Tree structure to update</param>
        public void AddToRollback(List<Difference> differenceList, List<TreeNode> arrNodes)
        {
            if (differenceList == null || differenceList.Count == 0 || arrNodes == null) //if there are no differences no need to do this
                return;

            List<Difference> differenceToRollbackList = new List<Difference>(); //differences to back up
            List<TreeNode> arrNodeList = arrNodes;
            FileInfo file;   

            foreach (Difference d in differenceList)    //process each difference found
            {
                if (d.GetItemType() == Type.File && d.GetPartnerFile() != null) //only accept files
                {
                    //only back up files that are going to be replaced on this system
                    if (d.GetPairType().First == DirectionType.HDToSystem)
                    {
                        //ignore creation and del-del conflicts as there's no existing copy to back up
                        if (d.GetPairType().Second == OperationType.Deletion ||
                            d.GetPairType().Second == OperationType.ModModConflict ||
                            d.GetPairType().Second == OperationType.CreationCreationConflict)
                        {
                            differenceToRollbackList.Add(d);
                        }

                        //Mod on other computer, Del on this computer
                        else if (d.GetPairType().Second == OperationType.Modification ||
                                 d.GetPairType().Second == OperationType.ModDeleteConflict)
                        {
                            file = d.GetSysFile();
                            if (file.Exists == true)    
                                differenceToRollbackList.Add(d);
                        }
                     }
                }
            }

            if (differenceToRollbackList.Count > 0) //if there are new rollback files to process
            {
                foreach (Difference d in differenceToRollbackList)
                {
                    ReviewRollbackFiles(d);   //free up space for new rollback files
                    AddRollbackFile(d);  //add new rollback files to list of rollback files
                    WriteRollbackMetadata();   //update rollback metadata
                }
            }
        }

       /// <summary>
        /// Deletes old files to make way for newer files
       /// </summary>
        private void ReviewRollbackFiles(Difference newDiff)
        {
            long spaceUsed = 0;

            //calculate space used by new file
            if (newDiff.GetItemType() == Type.File)
            {
                if (currentSystem == SystemType.Host)
                    spaceUsed += newDiff.GetPartnerFile().GetSizeHost();
                else
                    spaceUsed += newDiff.GetPartnerFile().GetSizeGuest();
            }

            //free up space by deleting older files
            if (spaceUsed > spaceRemaining && spaceUsed <= SpaceAllocated)
            {
                RollbackFile rbFile;

                while (spaceUsed > spaceRemaining)
                {
                    if (rbFileList.Count > 0)
                    {
                        rbFile = rbFileList.First();    //choose oldest file first
                        try
                        {
                            File.Delete(rbFile.RbDestination);   //delete physical file
                        }
                        catch (DirectoryNotFoundException)
                        {
                            throw new DirectoryNotFoundException(rbFile.RbDestination, ActionType.Rollback);
                        }
                        catch (UnauthorizedAccessException)
                        {
                            throw;
                        }

                        rbFileList.Remove(rbFile);  //Remove from list of rollback files
                        spaceRemaining += rbFile.RbSize;    //update space remaining
                    }
                }
            }
        }

       /// <summary>
        /// Adds a new rollback file to list of rollback files
       /// </summary>
        private void AddRollbackFile(Difference newDiff)
        {
            RollbackFile rbFile = new RollbackFile();    //create a new rollback file
            int fileVersion = 0;    //version of rollback file
            long spaceUsed = 0;
           
            rbFile.RbId = rbFileId; //id of rollback file
            rbFile.RbDone = false;  //rollback file is not written to metadata yet
            rbFileId++; //update id for next new rollback file

            //update new rollback file details
            if (currentSystem == SystemType.Host)  
            {
                spaceUsed = newDiff.GetPartnerFile().GetSizeHost();   
                rbFile.RbDestination = newDiff.GetPartnerFile().GetFullPathHost();
                rbFile.RbLastSyncTime = newDiff.GetPartnerFile().GetSyncHost();
                rbFile.RbSize = newDiff.GetPartnerFile().GetSizeHost();
            }

            else if (currentSystem == SystemType.Guest)   
            {
                spaceUsed = newDiff.GetPartnerFile().GetSizeGuest();  
                rbFile.RbDestination = newDiff.GetPartnerFile().GetFullPathGuest();
                rbFile.RbLastSyncTime = newDiff.GetPartnerFile().GetSyncGuest();
                rbFile.RbSize = newDiff.GetPartnerFile().GetSizeGuest();
            }
            
            foreach (RollbackFile r in rbFileList)
            {
                if (r.RbDestination.Equals(rbFile.RbDestination))  //check for exising file
                {
                    fileVersion = r.RbVersion + 1;   //update version of latest file added
                    rbFile.RbVersion = fileVersion;
                }
            }

            //create unique name for rollback file using id
            rbFile.RbSource = rbLocation + @"\" + rbFile.RbId + rbFile.RbVersion + newDiff.GetPartnerFile().GetName();

            if (File.Exists(rbFile.RbDestination) == true)   //file must exist on this computer
            {
                try
                {
                    File.Copy(rbFile.RbDestination, rbFile.RbSource); //make a rollback copy of file
                }
                catch (UnauthorizedAccessException)
                {
                    throw new UnauthorizedAccessException(rbFile.RbDestination, ActionType.Rollback);
                }
            }
            else
            {
                throw new FileNotFoundException(rbFile.RbDestination, ActionType.Rollback);
            }

            spaceRemaining -= spaceUsed;    //update space remaining
            rbFileList.Add(rbFile); //update list of rollback files
        }

       /// <summary>
        /// Updates rollback files with new root address
       /// </summary>
       /// <param name="oldRootAddress">Old root address</param>
       /// <param name="newRootAddress">New root address</param>
        public void UpdateRollbackFile(String oldRootAddress, String newRootAddress)
        {
            String address;
            int lengthOfAddress = oldRootAddress.Length; 
            foreach (RollbackFile r in rbFileList)
            {
                address = r.RbDestination.Remove(0,lengthOfAddress);
                address = newRootAddress + address;
                r.RbDestination = address;
            }
        }

       /// <summary>
        /// Performs rollback of a file
       /// </summary>
       /// <param name="synctool">Current SyncTool</param>
       /// <param name="filesToRollback">List of files to rollback</param>
       /// <param name="arrNodes">Tree structure to update</param>
        public void PerformRollback(SyncTool synctool, List<String> filesToRollback, List<TreeNode> arrNodes)
        {
            //List<Difference> differenceToSync = new List<Difference>(); //list of differences to sync
            TreeNode tNode;
            TreeOperation treeOp;
            String parseDateTime;
            int parentIndex;
            FileInfo fileInfo;
            String hdPath = ""; //path in hard disk to store file to sync
            String fileName;
            string source;  //location of rollback file
            string destination; //location to rollback to
            string destinationFolder;
            int nodeIndex = 0;

            foreach (String filePath in filesToRollback)   //list of files to rollback
            {
                foreach (RollbackFile r in rbFileList)
                {
                    if (r.RbSource.Equals(filePath)) //find file in metadata and retrieve its information
                    {
                        source = r.RbSource;
                        destination = r.RbDestination;
                        destinationFolder = destination.Substring(0, destination.LastIndexOf("\\"));
                        fileName = Utility.StripString(destination);    //create unique filename to store for syncing
                        hdPath = Path.Combine(currentProfile.ProfilePath, "Files");
                        hdPath = Path.Combine(hdPath, fileName);

                        nodeIndex = GetTreeNodeIndex(0, destination, arrNodes); //find index of file in tree structure

                        if (nodeIndex < 0)    //actual file is missing and needs to be restored
                        {
                            treeOp = new TreeOperation(arrNodes, synctool.GetSecondTime(), currentSystem);

                            String folderPath = destination;
                            String[] arrWords = folderPath.Split('\\');
                            String currentPath = arrWords[0];
                            DirectoryInfo dirInfo;
                            parentIndex = GetTreeNodeIndex(0, synctool.GetRootAddress(), arrNodes);

                            for (int i = 1; i < arrWords.Length-1; i++)
                            {
                                currentPath += "\\" + arrWords[i];
                                if (!Directory.Exists(currentPath))
                                {
                                    dirInfo = new DirectoryInfo(currentPath);
                                    Directory.CreateDirectory(currentPath);   //create directories if they are missing
                                    tNode = treeOp.folderUpdateArrNode(dirInfo, parentIndex);
                                    parentIndex = GetTreeNodeIndex(parentIndex, currentPath, arrNodes);
                                }
                                else
                                    parentIndex = GetTreeNodeIndex(parentIndex, currentPath, arrNodes);
                            }

                            //do actual rollback
                            try
                            {
                                File.Copy(source, hdPath);  //copy rollback file from rollback folder to hard disk
                            }
                            catch (FileNotFoundException)
                            {
                                throw new FileNotFoundException(source, ActionType.Rollback);
                            }
                            catch (DirectoryNotFoundException)
                            {
                                throw new DirectoryNotFoundException(source, ActionType.Rollback);
                            }
                            catch (UnauthorizedAccessException)
                            {
                                throw new UnauthorizedAccessException(source,ActionType.Rollback);
                            }

                            try
                            {
                                File.Move(source, destination); //move rollback file from rollback folder to current location
                            }
                            catch (FileNotFoundException)
                            {
                                throw new FileNotFoundException(source, ActionType.Rollback);
                            }
                            catch (DirectoryNotFoundException)
                            {
                                throw new DirectoryNotFoundException(source, ActionType.Rollback);
                            }
                            catch (UnauthorizedAccessException)
                            {
                                throw new UnauthorizedAccessException(source, ActionType.Rollback);
                            }

                            fileInfo = new FileInfo(destination);
                            tNode = treeOp.fileUpdateArrNode(fileInfo, parentIndex);    //update Tzync metadata to create file on other computer
                        }
                        else    //actual file is present and needs to be replaced
                        {
                            //do actual rollback
                            try
                            {
                                File.Copy(source, hdPath);  //copy rollback file from rollback folder to hard disk
                            }
                            catch (FileNotFoundException)
                            {
                                throw new FileNotFoundException(source, ActionType.Rollback);
                            }
                            catch (DirectoryNotFoundException)
                            {
                                throw new DirectoryNotFoundException(source, ActionType.Rollback);
                            }
                            catch (UnauthorizedAccessException)
                            {
                                throw new UnauthorizedAccessException(source, ActionType.Rollback);
                            }

                            if (File.Exists(destination))
                                File.Delete(destination);   //delete current file from current location

                            try
                            {
                                File.Move(source, destination); //move rollback file from rollback folder to current location
                            }
                            catch (FileNotFoundException)
                            {
                                throw new FileNotFoundException(source, ActionType.Rollback);
                            }
                            catch (DirectoryNotFoundException)
                            {
                                throw new DirectoryNotFoundException(source, ActionType.Rollback);
                            }
                            catch (UnauthorizedAccessException)
                            {
                                throw new UnauthorizedAccessException(source, ActionType.Rollback);
                            }

                            tNode = arrNodes[nodeIndex];
                            
                            //update Tzync metadata
                            if (currentSystem == SystemType.Host)  //host
                            {
                                if (arrNodes[nodeIndex].GetVersionHost().First == -1 && arrNodes[nodeIndex].GetVersionHost().Second == 0)
                                    arrNodes[nodeIndex].GetVersionHost().First++;

                                arrNodes[nodeIndex].GetVersionHost().First++;
                                fileInfo = new FileInfo(tNode.GetFullPathHost());
                                parseDateTime = Utility.ParseDateTime(fileInfo.LastWriteTime);
                                arrNodes[nodeIndex].SetSyncHost(parseDateTime);
                            }
                            else    //guest
                            {
                                if (arrNodes[nodeIndex].GetVersionGuest().First == 0 && arrNodes[nodeIndex].GetVersionGuest().Second == -1)
                                    arrNodes[nodeIndex].GetVersionGuest().Second++;

                                arrNodes[nodeIndex].GetVersionGuest().Second++;
                                fileInfo = new FileInfo(tNode.GetFullPathGuest());
                                parseDateTime = Utility.ParseDateTime(fileInfo.LastWriteTime);
                                arrNodes[nodeIndex].SetSyncGuest(parseDateTime);
                            }
                        }

                        spaceRemaining += r.RbSize;  //update space remaining
                        r.RbDone = true;  //rollback entry can be deleted from metadata
                        rbFileId--;
                        break;
                    }
                }
            }

            WriteRollbackMetadata();   //update rollback metadata
        }

       /// <summary>
        /// Checks if there's enough space for rollback files to be stored
       /// </summary>
        private void AdjustSpaceAllocated()
        {
            DriveInfo cDrive = new DriveInfo("C");  //C drive on computer
            long spaceAvailable = cDrive.AvailableFreeSpace;    //available space                

            if (spaceRemaining > spaceAvailable)    //check if user has enough space on computer for rollback
                spaceRemaining = spaceAvailable - spaceBuffer;  //adjust allocated space if necessary
        }

       /// <summary>
       /// Retrieves path to rollback to for a particular rollback file
       /// </summary>
       /// <param name="filePath">File path to search for</param>
        public String GetRollbackFilePath(String filePath)
        {
            ReadRollbackMetadata();

            foreach (RollbackFile r in rbFileList)
            {
                if (r.RbDestination.Equals(filePath))
                    return r.RbSource;
            }

            return "";
        }

       /// <summary>
        /// Finds the index of a file or folder in the tree structure
       /// </summary>
       /// <param name="startIndex">Index to start from</param>
       /// <param name="filename">Path of file to search</param>
       /// <param name="arrNodes">Tree structure to search</param>
        public int GetTreeNodeIndex(int startIndex, String path, List<TreeNode> arrNodes)
        {
            int index = -1; //returns -1 if folder/file is not found

            if (startIndex == -1)
                startIndex = 0;

            List<int> childrenIndex = arrNodes[startIndex].GetChilds();

            if (arrNodes[0].Isdeleted == false) //only check non-deleted folders
            {
                if (currentSystem == SystemType.Host)   //PC A
                {
                    if (path.Equals(arrNodes[0].GetFullPathHost()))    //check root folder
                    {
                        index = 0;
                        return index;
                    }
                }
                else    //PC B
                {
                    if (path.Equals(arrNodes[0].GetFullPathGuest()))    //check root folder
                    {
                        index = 0;
                        return index;
                    }
                }
            }

            for (int i = 0; i < childrenIndex.Count()-1; i++)
            {
                if (arrNodes[childrenIndex[i]].Isdeleted == false)  //only check non-deleted files/folders
                {
                    if (currentSystem == SystemType.Host)   //PC A
                    {
                        if (arrNodes[childrenIndex[i]].GetNodeType() == Type.File)  //check files
                        {
                            if (path.Equals(arrNodes[childrenIndex[i]].GetFullPathHost())) //file found
                            {
                                index = childrenIndex[i];
                                break;
                            }
                        }
                        else    //check folders
                        {
                            
                            if (path.Equals(arrNodes[childrenIndex[i]].GetFullPathHost())) //folder found
                            {
                                index = childrenIndex[i];
                                break;
                            }
                            else
                            {
                                //check if filename is a prefix of parent folder and current item is not a folder
                                if (path.Contains(arrNodes[childrenIndex[i]].GetFullPathHost()) &&
                                    path.ElementAt(arrNodes[childrenIndex[i]].GetFullPathHost().Length) != '\\')
                                    index = GetTreeNodeIndex(childrenIndex[i], path, arrNodes);   //go into folder
                            }
                        }
                    }
                    else    //PC B
                    {
                        if (arrNodes[childrenIndex[i]].GetNodeType() == Type.File)   //check files
                        {
                            if (path.Equals(arrNodes[childrenIndex[i]].GetFullPathGuest())) //file found
                            {
                                index = childrenIndex[i];
                                break;
                            }
                        }
                        else    //check folders
                        {
                            if (path.Equals(arrNodes[childrenIndex[i]].GetFullPathGuest())) //folder found
                            {
                                index = childrenIndex[i];
                                break;
                            }
                            else
                            {
                                //check if filename is a prefix of parent folder and current item is not a folder
                                if (path.Contains(arrNodes[childrenIndex[i]].GetFullPathGuest()) &&
                                    path.ElementAt(arrNodes[childrenIndex[i]].GetFullPathGuest().Length) != '\\')
                                    index = GetTreeNodeIndex(childrenIndex[i], path, arrNodes);
                            }
                        }
                    }
                }
            }

            return index;
        }

       //Get and Set methods
        public List<RollbackFile> RbFileList
        {
            get { return rbFileList; }
            set { rbFileList = value; }
        }

        public long SpaceAllocated
        {
            get { return spaceAllocated; }
            set { spaceAllocated = value; }
        }

        public long SpaceRemaining
        {
            get { return spaceRemaining; }
            set { spaceRemaining = value; }
        }

    }   //end of Rollback class
}